package demo.java.nio.file;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.AclEntry;
import java.nio.file.attribute.AclEntryFlag;
import java.nio.file.attribute.AclEntryPermission;
import java.nio.file.attribute.AclEntryType;
import java.nio.file.attribute.AclFileAttributeView;
import java.nio.file.attribute.UserPrincipal;
import java.nio.file.attribute.UserPrincipalLookupService;
import java.nio.file.attribute.UserPrincipalNotFoundException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * Sample utility for editing a file's ACL (Access Control List).
 */
public class AclEdit {

    // parse string as list of ACE permissions separated by /
    static Set<AclEntryPermission> parsePermissions(String permsString) {
        Set<AclEntryPermission> perms = new HashSet<AclEntryPermission>();
        String[] result = permsString.split("/");
        for (String s : result) {
            if (s.equals(""))
                continue;
            try {
                perms.add(AclEntryPermission.valueOf(s.toUpperCase()));
            } catch (IllegalArgumentException x) {
                System.err.format("Invalid permission '%s'\n", s);
                System.exit(-1);
            }
        }
        return perms;
    }

    // parse string as list of ACE flags separated by /
    static Set<AclEntryFlag> parseFlags(String flagsString) {
        Set<AclEntryFlag> flags = new HashSet<AclEntryFlag>();
        String[] result = flagsString.split("/");
        for (String s : result) {
            if (s.equals(""))
                continue;
            try {
                flags.add(AclEntryFlag.valueOf(s.toUpperCase()));
            } catch (IllegalArgumentException x) {
                System.err.format("Invalid flag '%s'\n", s);
                System.exit(-1);
            }
        }
        return flags;
    }

    // parse ACE type
    static AclEntryType parseType(String typeString) {
        // FIXME: support audit and alarm types in the future
        if (typeString.equalsIgnoreCase("allow"))
            return AclEntryType.ALLOW;
        if (typeString.equalsIgnoreCase("deny"))
            return AclEntryType.DENY;
        System.err.format("Invalid type '%s'\n", typeString);
        System.exit(-1);
        return null; // keep compiler happy
    }

    /**
     * Parse string of the form: [user|group:]<username|groupname>:<perms>[:flags]:<allow|deny>
     */
    static AclEntry parseAceString(String s, UserPrincipalLookupService lookupService) {
        String[] result = s.split(":");

        // must have at least 3 components (username:perms:type)
        if (result.length < 3)
            usage();

        int index = 0;
        int remaining = result.length;

        // optional first component can indicate user or group type
        boolean isGroup = false;
        if (result[index].equalsIgnoreCase("user") || result[index].equalsIgnoreCase("group")) {
            if (--remaining < 3)
                usage();
            isGroup = result[index++].equalsIgnoreCase("group");
        }

        // user and permissions required
        String userString = result[index++];
        remaining--;
        String permsString = result[index++];
        remaining--;

        // flags are optional
        String flagsString = "";
        String typeString = null;
        if (remaining == 1) {
            typeString = result[index++];
        } else {
            if (remaining == 2) {
                flagsString = result[index++];
                typeString = result[index++];
            } else {
                usage();
            }
        }

        // lookup UserPrincipal
        UserPrincipal user = null;
        try {
            user = (isGroup) ? lookupService.lookupPrincipalByGroupName(userString)
                    : lookupService.lookupPrincipalByName(userString);
        } catch (UserPrincipalNotFoundException x) {
            System.err.format("Invalid %s '%s'\n", ((isGroup) ? "group" : "user"), userString);
            System.exit(-1);
        } catch (IOException x) {
            System.err.format("Lookup of '%s' failed: %s\n", userString, x);
            System.exit(-1);
        }

        // map string representation of permissions, flags, and type
        Set<AclEntryPermission> perms = parsePermissions(permsString);
        Set<AclEntryFlag> flags = parseFlags(flagsString);
        AclEntryType type = parseType(typeString);

        // build the ACL entry
        return AclEntry.newBuilder().setType(type).setPrincipal(user).setPermissions(perms).setFlags(flags).build();
    }

    static void usage() {
        System.err.println("usage: java AclEdit [ACL-operation] file");
        System.err.println("");
        System.err.println("Example 1: Prepends access control entry to the begining of the myfile's ACL");
        System.err.println("       java AclEdit A+alice:read_data/read_attributes:allow myfile");
        System.err.println("");
        System.err.println("Example 2: Remove the entry at index 6 of myfile's ACL");
        System.err.println("       java AclEdit A6- myfile");
        System.err.println("");
        System.err.println("Example 3: Replace the entry at index 2 of myfile's ACL");
        System.err.println("       java AclEdit A2=bob:write_data/append_data:deny myfile");
        System.exit(-1);
    }

    static enum Action {
        PRINT, ADD, REMOVE, REPLACE;
    }

    /**
     * Main class: parses arguments and prints or edits ACL
     */
    public static void main(String[] args) throws IOException {
        Action action = null;
        int index = -1;
        String entryString = null;

        // parse arguments
        if (args.length < 1 || args[0].equals("-help") || args[0].equals("-?"))
            usage();

        if (args.length == 1) {
            action = Action.PRINT;
        } else {
            String s = args[0];

            // A[index]+entry
            if (Pattern.matches("^A[0-9]*\\+.*", s)) {
                String[] result = s.split("\\+", 2);
                if (result.length == 2) {
                    if (result[0].length() < 2) {
                        index = 0;
                    } else {
                        index = Integer.parseInt(result[0].substring(1));
                    }
                    entryString = result[1];
                    action = Action.ADD;
                }
            }

            // Aindex-
            if (Pattern.matches("^A[0-9]+\\-", s)) {
                String[] result = s.split("\\-", 2);
                if (result.length == 2) {
                    index = Integer.parseInt(result[0].substring(1));
                    entryString = result[1];
                    action = Action.REMOVE;
                }
            }

            // Aindex=entry
            if (Pattern.matches("^A[0-9]+=.*", s)) {
                String[] result = s.split("=", 2);
                if (result.length == 2) {
                    index = Integer.parseInt(result[0].substring(1));
                    entryString = result[1];
                    action = Action.REPLACE;
                }
            }
        }
        if (action == null)
            usage();

        int fileArg = (action == Action.PRINT) ? 0 : 1;
        Path file = Paths.get(args[fileArg]);

        // read file's ACL
        AclFileAttributeView view = Files.getFileAttributeView(file, AclFileAttributeView.class);
        if (view == null) {
            System.err.println("ACLs not supported on this platform");
            System.exit(-1);
        }
        List<AclEntry> acl = view.getAcl();

        switch (action) {
        // print ACL
        case PRINT: {
            for (int i = 0; i < acl.size(); i++) {
                System.out.format("%5d: %s\n", i, acl.get(i));
            }
            break;
        }

        // add ACE to existing ACL
        case ADD: {
            AclEntry entry = parseAceString(entryString, file.getFileSystem().getUserPrincipalLookupService());
            if (index >= acl.size()) {
                acl.add(entry);
            } else {
                acl.add(index, entry);
            }
            view.setAcl(acl);
            break;
        }

        // remove ACE
        case REMOVE: {
            if (index >= acl.size()) {
                System.err.format("Index '%d' is invalid", index);
                System.exit(-1);
            }
            acl.remove(index);
            view.setAcl(acl);
            break;
        }

        // replace ACE
        case REPLACE: {
            if (index >= acl.size()) {
                System.err.format("Index '%d' is invalid", index);
                System.exit(-1);
            }
            AclEntry entry = parseAceString(entryString, file.getFileSystem().getUserPrincipalLookupService());
            acl.set(index, entry);
            view.setAcl(acl);
            break;
        }
        }
    }
}
